package org.giovanolli.mumi.dao.impl;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.CollectionUtils;
import org.giovanolli.mumi.entities.filter.EntityFilter;
import org.giovanolli.mumi.entities.filter.SortCriteria;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * The Class HibernateDao.
 * 
 * @param <T>
 *            the generic type
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public abstract class HibernateDao<T> {

	/** The session factory. */
	private SessionFactory sessionFactory;

	/** The default sort criteria. */
	private List<SortCriteria> defaultSortCriteria = new ArrayList<SortCriteria>();

	/**
	 * Instantiates a new hibernate dao.
	 */
	public HibernateDao() {
		super();
		setDefaultSortCriteria(defaultSortCriteria);
	}

	/**
	 * Delete.
	 * 
	 * @param object
	 *            the object
	 */
	public void delete(T object) {
		getCurrentSession().delete(object);
	}

	/**
	 * Evict.
	 * 
	 * @param entity
	 *            the entity
	 */
	public void evict(Object entity) {
		getCurrentSession().evict(entity);
	}

	/**
	 * Fill criteria.
	 * 
	 * @param filter
	 *            the filter
	 * @param criteria
	 *            the criteria
	 * @return the detached criteria
	 */
	protected abstract DetachedCriteria fillCriteria(EntityFilter<T> filter, DetachedCriteria criteria);

	/**
	 * Fill sort criteria.
	 * 
	 * @param filter
	 *            the filter
	 * @param criteria
	 *            the criteria
	 * @return the detached criteria
	 */
	private DetachedCriteria fillSortCriteria(EntityFilter<T> filter, DetachedCriteria criteria) {
		if (CollectionUtils.isEmpty(filter.getSortCriterias())) {
			filter.setSortCriterias(defaultSortCriteria);
		}
		for (SortCriteria sortCriteria : filter.getSortCriterias()) {
			criteria.addOrder(sortCriteria.getAsc() ? Order.asc(sortCriteria.getAttribute()) : Order.desc(sortCriteria.getAttribute()));
		}
		return criteria;
	}

	/**
	 * Find.
	 * 
	 * @param query
	 *            the query
	 * @param paramMap
	 *            the param map
	 * @return the list
	 */
	public List<T> find(String query, Map<String, Object> paramMap) {
		final Query queryObject = getCurrentSession().createQuery(query);
		// prepareQuery(queryObject);
		if (!paramMap.isEmpty() && queryObject.getNamedParameters().length > 0) {
			for (int i = 0; i < queryObject.getNamedParameters().length; i++) {
				String paramName = queryObject.getNamedParameters()[i];
				Object paramValue = paramMap.get(queryObject.getNamedParameters()[i]);
				if (paramValue == null) {
					throw new IllegalStateException("Invalid Parameter value: " + paramName);
				}
				queryObject.setParameter(paramName, paramValue);
			}
		}

		return queryObject.list();
	}

	/**
	 * Find by criteria.
	 * 
	 * @param criteria
	 *            the criteria
	 * @return the list
	 */
	public List<T> findByCriteria(DetachedCriteria criteria) {
		return this.findByCriteria(criteria, 0, 0);
	}

	/**
	 * Find by criteria.
	 * 
	 * @param criteria
	 *            the criteria
	 * @param firstResult
	 *            the first result
	 * @param maxResults
	 *            the max results
	 * @return the list
	 */
	public List findByCriteria(final DetachedCriteria criteria, final int firstResult, final int maxResults) {
		Criteria executableCriteria = criteria.getExecutableCriteria(getCurrentSession());
		// prepareCriteria(executableCriteria);
		if (firstResult >= 0) {
			executableCriteria.setFirstResult(firstResult);
		}
		if (maxResults > 0) {
			executableCriteria.setMaxResults(maxResults);
		}
		return executableCriteria.list();
	}

	/**
	 * Find unique.
	 * 
	 * @param query
	 *            the query
	 * @param paramMap
	 *            the param map
	 * @return the t
	 */
	public T findUnique(String query, Map<String, Object> paramMap) {

		List results = this.find(query, paramMap);

		if (results.isEmpty()) {
			return null;
		}
		if (results.size() > 1) {
			throw new IllegalStateException("Invalid Number of rows returned for unique call: " + results.size());
		}
		return (T) results.get(0);
	}

	/**
	 * Gets the all.
	 * 
	 * @param <T1>
	 *            the generic type
	 * @param entityClass
	 *            the entity class
	 * @return the all
	 */
	public <T1> List<T1> getAll(Class<T1> entityClass) {
		Criteria criteria = getCurrentSession().createCriteria(entityClass);
		criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
		// prepareCriteria(criteria);
		return criteria.list();
	}

	/**
	 * Gets the by id.
	 * 
	 * @param id
	 *            the id
	 * @return the by id
	 */
	public T getById(Serializable id) {
		return (T) getCurrentSession().get(getGenericType(), id);
	}

	/**
	 * Gets the by uid.
	 * 
	 * @param uid
	 *            the uid
	 * @return the by uid
	 */
	public T getByUid(String uid) {
		return (T) getCurrentSession().createCriteria(getGenericType()).add(Restrictions.eq("uid", uid)).uniqueResult();
	}

	/**
	 * Gets the count.
	 * 
	 * @param filter
	 *            the filter
	 * @return the count
	 */
	public int getCount(EntityFilter<T> filter) {
		List<Number> result = null;

		final Session session = getCurrentSession();

		final DetachedCriteria dCriteria = DetachedCriteria.forClass(getGenericType());

		fillCriteria(filter, dCriteria);
		fillSortCriteria(filter, dCriteria);
		final Criteria executableCriteria = dCriteria.getExecutableCriteria(session);
		executableCriteria.setProjection(Projections.rowCount());

		result = executableCriteria.list();
		return result.get(0).intValue();
	}

	/**
	 * Gets the current session.
	 * 
	 * @return the current session
	 */
	public Session getCurrentSession() {
		return sessionFactory.getCurrentSession();
	}

	/**
	 * Gets the generic type.
	 * 
	 * @return the generic type
	 */
	private Class<T> getGenericType() {
		return (Class<T>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
	}

	/**
	 * Gets the list.
	 * 
	 * @param filter
	 *            the filter
	 * @return the list
	 */
	public List<T> getList(EntityFilter<T> filter) {
		return getList(filter, -1, -1);
	}

	/**
	 * Gets the list.
	 * 
	 * @param filter
	 *            the filter
	 * @param firstResult
	 *            the first result
	 * @param maxResults
	 *            the max results
	 * @return the list
	 */
	public List<T> getList(EntityFilter<T> filter, int firstResult, int maxResults) {

		final Session session = getCurrentSession();

		final DetachedCriteria dCriteria = DetachedCriteria.forClass(getGenericType());

		fillCriteria(filter, dCriteria);
		fillSortCriteria(filter, dCriteria);
		Criteria executableCriteria = dCriteria.getExecutableCriteria(session);

		// prepareCriteria(executableCriteria);
		if (firstResult >= 0) {
			executableCriteria.setFirstResult(firstResult);
		}
		if (maxResults > 0) {
			executableCriteria.setMaxResults(maxResults);
		}
		return executableCriteria.list();
	}

	/**
	 * Save.
	 * 
	 * @param object
	 *            the object
	 */
	public void save(T object) {
		getCurrentSession().saveOrUpdate(object);
	}

	/**
	 * Sets the default sort criteria.
	 * 
	 * @param defaultSortCriteria
	 *            the new default sort criteria
	 */
	protected void setDefaultSortCriteria(List<SortCriteria> defaultSortCriteria) {

	}

	/**
	 * Sets the session factory.
	 * 
	 * @param sessionFactory
	 *            the new session factory
	 */
	@Autowired
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}
}
